From 7a7edd69ef37a69f1f06dc732c2df6b9e974556f Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Thu, 23 Jan 2025 22:17:52 +0100 Subject: [PATCH] automatic removing of invalid spaces in file names to ensure compatibility with Widnows, we will remove automatically the leading space characters in file name of new files or folders Signed-off-by: Matthieu Gallien --- src/libsync/discovery.cpp | 37 ++++++++++++++++++ src/libsync/discovery.h | 4 ++ src/libsync/discoveryphase.h | 1 + src/libsync/syncengine.cpp | 6 +++ src/libsync/syncengine.h | 2 + test/testsyncengine.cpp | 72 ++++++++++++++++++++++++++++++++++++ 6 files changed, 122 insertions(+) diff --git a/src/libsync/discovery.cpp b/src/libsync/discovery.cpp index 75abf7ca4..646808147 100644 --- a/src/libsync/discovery.cpp +++ b/src/libsync/discovery.cpp @@ -420,14 +420,17 @@ bool ProcessDirectoryJob::handleExcluded(const QString &path, const Entries &ent case CSYNC_FILE_EXCLUDE_TRAILING_SPACE: item->_errorString = tr("Filename contains trailing spaces."); item->_status = SyncFileItem::FileNameInvalid; + maybeRenameForWindowsCompatibility(_discoveryData->_localDir + item->_file, excluded); break; case CSYNC_FILE_EXCLUDE_LEADING_SPACE: item->_errorString = tr("Filename contains leading spaces."); item->_status = SyncFileItem::FileNameInvalid; + maybeRenameForWindowsCompatibility(_discoveryData->_localDir + item->_file, excluded); break; case CSYNC_FILE_EXCLUDE_LEADING_AND_TRAILING_SPACE: item->_errorString = tr("Filename contains leading and trailing spaces."); item->_status = SyncFileItem::FileNameInvalid; + maybeRenameForWindowsCompatibility(_discoveryData->_localDir + item->_file, excluded); break; case CSYNC_FILE_EXCLUDE_LONG_FILENAME: item->_errorString = tr("Filename is too long."); @@ -2274,4 +2277,38 @@ void ProcessDirectoryJob::setupDbPinStateActions(SyncJournalFileRecord &record) } } +void ProcessDirectoryJob::maybeRenameForWindowsCompatibility(const QString &absoluteFileName, + CSYNC_EXCLUDE_TYPE excludeReason) +{ + if (!_discoveryData->_shouldEnforceWindowsFileNameCompatibility) { + return; + } + + const auto fileInfo = QFileInfo{absoluteFileName}; + switch (excludeReason) + { + case CSYNC_NOT_EXCLUDED: + case CSYNC_FILE_EXCLUDE_CASE_CLASH_CONFLICT: + case CSYNC_FILE_EXCLUDE_AND_REMOVE: + case CSYNC_FILE_EXCLUDE_CANNOT_ENCODE: + case CSYNC_FILE_EXCLUDE_INVALID_CHAR: + case CSYNC_FILE_EXCLUDE_SERVER_BLACKLISTED: + case CSYNC_FILE_EXCLUDE_HIDDEN: + case CSYNC_FILE_SILENTLY_EXCLUDED: + case CSYNC_FILE_EXCLUDE_STAT_FAILED: + case CSYNC_FILE_EXCLUDE_LONG_FILENAME: + case CSYNC_FILE_EXCLUDE_LIST: + case CSYNC_FILE_EXCLUDE_CONFLICT: + break; + case CSYNC_FILE_EXCLUDE_LEADING_AND_TRAILING_SPACE: + case CSYNC_FILE_EXCLUDE_LEADING_SPACE: + case CSYNC_FILE_EXCLUDE_TRAILING_SPACE: + { + const auto renameTarget = QString{fileInfo.absolutePath() + QStringLiteral("/") + fileInfo.fileName().trimmed()}; + FileSystem::rename(absoluteFileName, renameTarget); + break; + } + } +} + } diff --git a/src/libsync/discovery.h b/src/libsync/discovery.h index 840eb15d6..d87114d8b 100644 --- a/src/libsync/discovery.h +++ b/src/libsync/discovery.h @@ -15,6 +15,7 @@ #pragma once #include +#include "csync_exclude.h" #include "discoveryphase.h" #include "syncfileitem.h" #include "common/asserts.h" @@ -255,6 +256,9 @@ private: */ void setupDbPinStateActions(SyncJournalFileRecord &record); + void maybeRenameForWindowsCompatibility(const QString &absoluteFileName, + CSYNC_EXCLUDE_TYPE excludeReason); + qint64 _lastSyncTimestamp = 0; QueryMode _queryServer = QueryMode::NormalQuery; diff --git a/src/libsync/discoveryphase.h b/src/libsync/discoveryphase.h index f1d1d3136..3382ffc13 100644 --- a/src/libsync/discoveryphase.h +++ b/src/libsync/discoveryphase.h @@ -326,6 +326,7 @@ public: QRegularExpression _invalidFilenameRx; // FIXME: maybe move in ExcludedFiles QStringList _serverBlacklistedFiles; // The blacklist from the capabilities QStringList _leadingAndTrailingSpacesFilesAllowed; + bool _shouldEnforceWindowsFileNameCompatibility = false; bool _ignoreHiddenFiles = false; std::function _shouldDiscoverLocaly; diff --git a/src/libsync/syncengine.cpp b/src/libsync/syncengine.cpp index 2ac90bf1e..d5018149b 100644 --- a/src/libsync/syncengine.cpp +++ b/src/libsync/syncengine.cpp @@ -647,6 +647,7 @@ void SyncEngine::startSync() _discoveryPhase = std::make_unique(); _discoveryPhase->_leadingAndTrailingSpacesFilesAllowed = _leadingAndTrailingSpacesFilesAllowed; + _discoveryPhase->_shouldEnforceWindowsFileNameCompatibility = _shouldEnforceWindowsFileNameCompatibility; _discoveryPhase->_account = _account; _discoveryPhase->_excludes = _excludedFiles.data(); const QString excludeFilePath = _localPath + QStringLiteral(".sync-exclude.lst"); @@ -1208,6 +1209,11 @@ void SyncEngine::addAcceptedInvalidFileName(const QString& filePath) _leadingAndTrailingSpacesFilesAllowed.append(filePath); } +void SyncEngine::setLocalDiscoveryEnforceWindowsFileNameCompatibility(bool value) +{ + _shouldEnforceWindowsFileNameCompatibility = value; +} + bool SyncEngine::wasFileTouched(const QString &fn) const { // Start from the end (most recent) and look for our path. Check the time just in case. diff --git a/src/libsync/syncengine.h b/src/libsync/syncengine.h index 2dc5bfa6b..76fc99ce7 100644 --- a/src/libsync/syncengine.h +++ b/src/libsync/syncengine.h @@ -159,6 +159,7 @@ public slots: */ void setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle style, std::set paths = {}); void addAcceptedInvalidFileName(const QString& filePath); + void setLocalDiscoveryEnforceWindowsFileNameCompatibility(bool value); signals: // During update, before reconcile @@ -409,6 +410,7 @@ private: std::set _localDiscoveryPaths; QStringList _leadingAndTrailingSpacesFilesAllowed; + bool _shouldEnforceWindowsFileNameCompatibility = false; // Hash of files we have scheduled for later sync runs, along with a // pointer to the timer which will trigger the sync run for it. diff --git a/test/testsyncengine.cpp b/test/testsyncengine.cpp index cecd9ae59..1a1d55971 100644 --- a/test/testsyncengine.cpp +++ b/test/testsyncengine.cpp @@ -2035,6 +2035,78 @@ private slots: QVERIFY(fakeFolder.syncOnce()); } + + void testCreateFileWithTrailingLeadingSpaces_local_automatedRenameBeforeUpload() + { + FakeFolder fakeFolder{FileInfo{}}; + fakeFolder.syncEngine().setLocalDiscoveryEnforceWindowsFileNameCompatibility(true); + + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + + const QString fileWithSpaces1(" foo"); + const QString fileWithSpaces2(" bar "); + const QString fileWithSpaces3("bla "); + const QString fileWithSpaces4("A/ foo"); + const QString fileWithSpaces5("A/ bar "); + const QString fileWithSpaces6("A/bla "); + const auto extraFileNameWithSpaces = QStringLiteral(" with spaces "); + const QString fileWithoutSpaces1("foo"); + const QString fileWithoutSpaces2("bar"); + const QString fileWithoutSpaces3("bla"); + const QString fileWithoutSpaces4("A/foo"); + const QString fileWithoutSpaces5("A/bar"); + const QString fileWithoutSpaces6("A/bla"); + const auto extraFileNameWithoutSpaces = QStringLiteral("with spaces"); + + fakeFolder.localModifier().insert(fileWithSpaces1); + fakeFolder.localModifier().insert(fileWithSpaces2); + fakeFolder.localModifier().insert(fileWithSpaces3); + fakeFolder.localModifier().mkdir("A"); + fakeFolder.localModifier().insert(fileWithSpaces4); + fakeFolder.localModifier().insert(fileWithSpaces5); + fakeFolder.localModifier().insert(fileWithSpaces6); + fakeFolder.localModifier().mkdir(extraFileNameWithSpaces); + + ItemCompletedSpy completeSpy(fakeFolder); + completeSpy.clear(); + + QVERIFY(fakeFolder.syncOnce()); + + QCOMPARE(completeSpy.findItem(fileWithSpaces1)->_status, SyncFileItem::Status::FileNameInvalid); + QCOMPARE(completeSpy.findItem(fileWithSpaces2)->_status, SyncFileItem::Status::FileNameInvalid); + QCOMPARE(completeSpy.findItem(fileWithSpaces3)->_status, SyncFileItem::Status::FileNameInvalid); + QCOMPARE(completeSpy.findItem(fileWithSpaces4)->_status, SyncFileItem::Status::FileNameInvalid); + QCOMPARE(completeSpy.findItem(fileWithSpaces5)->_status, SyncFileItem::Status::FileNameInvalid); + QCOMPARE(completeSpy.findItem(fileWithSpaces6)->_status, SyncFileItem::Status::FileNameInvalid); + QCOMPARE(completeSpy.findItem(extraFileNameWithSpaces)->_status, SyncFileItem::Status::FileNameInvalid); + QCOMPARE(completeSpy.findItem(fileWithoutSpaces1)->_status, SyncFileItem::Status::NoStatus); + QCOMPARE(completeSpy.findItem(fileWithoutSpaces2)->_status, SyncFileItem::Status::NoStatus); + QCOMPARE(completeSpy.findItem(fileWithoutSpaces3)->_status, SyncFileItem::Status::NoStatus); + QCOMPARE(completeSpy.findItem(fileWithoutSpaces4)->_status, SyncFileItem::Status::NoStatus); + QCOMPARE(completeSpy.findItem(fileWithoutSpaces5)->_status, SyncFileItem::Status::NoStatus); + QCOMPARE(completeSpy.findItem(fileWithoutSpaces6)->_status, SyncFileItem::Status::NoStatus); + QCOMPARE(completeSpy.findItem(extraFileNameWithoutSpaces)->_status, SyncFileItem::Status::NoStatus); + + completeSpy.clear(); + + fakeFolder.syncEngine().setLocalDiscoveryOptions(LocalDiscoveryStyle::DatabaseAndFilesystem, {QStringLiteral("foo"), QStringLiteral("bar"), QStringLiteral("bla"), QStringLiteral("A/foo"), QStringLiteral("A/bar"), QStringLiteral("A/bla")}); + QVERIFY(fakeFolder.syncOnce()); + + QCOMPARE(completeSpy.findItem(fileWithSpaces1)->_status, SyncFileItem::Status::NoStatus); + QCOMPARE(completeSpy.findItem(fileWithSpaces2)->_status, SyncFileItem::Status::NoStatus); + QCOMPARE(completeSpy.findItem(fileWithSpaces3)->_status, SyncFileItem::Status::NoStatus); + QCOMPARE(completeSpy.findItem(fileWithSpaces4)->_status, SyncFileItem::Status::NoStatus); + QCOMPARE(completeSpy.findItem(fileWithSpaces5)->_status, SyncFileItem::Status::NoStatus); + QCOMPARE(completeSpy.findItem(fileWithSpaces6)->_status, SyncFileItem::Status::NoStatus); + QCOMPARE(completeSpy.findItem(extraFileNameWithSpaces)->_status, SyncFileItem::Status::NoStatus); + QCOMPARE(completeSpy.findItem(fileWithoutSpaces1)->_status, SyncFileItem::Status::Success); + QCOMPARE(completeSpy.findItem(fileWithoutSpaces2)->_status, SyncFileItem::Status::Success); + QCOMPARE(completeSpy.findItem(fileWithoutSpaces3)->_status, SyncFileItem::Status::Success); + QCOMPARE(completeSpy.findItem(fileWithoutSpaces4)->_status, SyncFileItem::Status::Success); + QCOMPARE(completeSpy.findItem(fileWithoutSpaces5)->_status, SyncFileItem::Status::Success); + QCOMPARE(completeSpy.findItem(fileWithoutSpaces6)->_status, SyncFileItem::Status::Success); + QCOMPARE(completeSpy.findItem(extraFileNameWithoutSpaces)->_status, SyncFileItem::Status::Success); + } }; QTEST_GUILESS_MAIN(TestSyncEngine) -- 2.30.2